package blue.http.internal.core.net;

import blue.http.core.annotation.HttpMethod;
import blue.http.core.message.HttpResponse;
import blue.http.core.message.UploadFile;
import blue.http.core.options.HttpOptions;
import blue.http.core.options.WebSocketOptions;
import blue.http.core.plugin.SessionProvider;
import blue.http.internal.core.handler.HandlerChain;
import blue.http.internal.core.handler.HandlerFactory;
import blue.http.internal.core.mapping.HandlerMappingFactory;
import blue.http.internal.core.message.DefaultHttpRequest;
import blue.http.internal.core.message.DefaultUploadFile;
import blue.http.internal.core.net.response.ResponseHandlerFactory;
import blue.http.internal.core.parser.HttpMethodResult;
import blue.http.internal.core.util.HttpServerUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutorService;

/**
 * @author Jin Zheng
 * @since 1.0 2020-01-03
 */
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
	private final static HttpDataFactory FACTORY = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);
	private static Logger logger = LoggerFactory.getLogger(HttpServerHandler.class);

	private final ExecutorService executor;
	private final HttpOptions httpOptions;
	private final WebSocketOptions webSocketOptions;
	private final HandlerMappingFactory handlerMappingFactory;
	private final HandlerFactory handlerFactory;
	private final ResponseHandlerFactory responseHandlerFactory;
	private final SessionProvider sessionProvider;

	private Channel channel;
	private HttpRequest httpRequest;
	private DefaultHttpRequest blueRequest;
	private HandlerChain chain;
	private StringBuilder contentBuilder = new StringBuilder();
	private Charset charset;
	private boolean isText = true;
	private boolean isHttp = true;
	private HttpPostRequestDecoder decoder;

	public HttpServerHandler(DefaultHttpServerBuilder builder) {
		this.executor = builder.getServerOptions().getExecutor();
		this.httpOptions = builder.getHttpOptions();
		this.webSocketOptions = builder.getWebSocketOptions();
		this.handlerMappingFactory = builder.getHandlerMappingFactory();
		this.handlerFactory = builder.getHandlerFactory();
		this.responseHandlerFactory = builder.getResponseHandlerFactory();
		this.sessionProvider = builder.getSessionProvider();
	}

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
		this.channel = ctx.channel();
		if (msg instanceof HttpRequest) {
			this.httpRequest = (HttpRequest) msg;
			this.handleRequest(ctx);
		} else if (msg instanceof HttpContent) {
			this.handleContent(ctx, (HttpContent) msg);
		}
	}

	private void handleRequest(ChannelHandlerContext ctx) {
		String wsRoot = webSocketOptions == null ? "" : webSocketOptions.getRoot();
		HttpMethod httpMethod = HttpServerUtil.getHttpMethod(ctx, httpRequest, wsRoot, httpOptions.getMaxUploadSize());
		if (httpMethod == null) {
			this.isHttp = false;
			return;
		}

		isText = HttpServerUtil.isPostText(httpRequest);
		String ip = HttpServerUtil.getIp(httpRequest.headers(), ctx.channel());
		blueRequest = new DefaultHttpRequest(httpMethod, ip, channel.id(), sessionProvider);
		blueRequest.setContentLength(HttpUtil.getContentLength(httpRequest, 0L));
		blueRequest.parseUri(httpRequest.uri(), httpOptions.getContextPath());
		blueRequest.parseHeaders(httpRequest.headers());
		chain = handlerMappingFactory.getHandlerChain(blueRequest);
		if (chain == null) {
			executor.execute(() -> HttpStaticHandler.handle(channel, httpRequest, httpOptions, blueRequest.getUrl()));
			return;
		}
		HttpMethodResult result = (HttpMethodResult) chain.getHandler();
		blueRequest.putPathVariable(result.getPathMap());
		charset = Charset.forName(result.getCharset().getName());
		decoder = new HttpPostRequestDecoder(FACTORY, httpRequest);
	}

	private void handleContent(ChannelHandlerContext ctx, HttpContent content) {
		if (!isHttp || httpRequest == null) {
			ctx.fireChannelRead(content.retain());
			return;
		}

		if (isText && blueRequest.getHttpMethod() == HttpMethod.POST) {
			String c = charset == null ? content.content().toString() : content.content().toString(charset);
			contentBuilder.append(c);
		}

		this.parsePostData(content);

		if (content instanceof LastHttpContent) {
			blueRequest.setContent(contentBuilder.toString());
			executor.execute(() ->
			{
				HttpResponse response = handlerFactory.handle(blueRequest, chain);
				responseHandlerFactory.handle(channel, httpRequest, response);
				decoder.destroy();
				decoder = null;
			});
		}
	}

	private void parsePostData(HttpContent content) {
		decoder.offer(content);
		try {
			while (decoder.hasNext()) {
				InterfaceHttpData data = decoder.next();
				if (data == null)
					continue;

				String name = data.getName();
				switch (data.getHttpDataType()) {
					case FileUpload:
						FileUpload fileUpload = (FileUpload) data;
						if (fileUpload.isCompleted()) {
							UploadFile uploadFile = new DefaultUploadFile(fileUpload);
							blueRequest.putFile(name, uploadFile);
							logger.info("File upload successful: {}", data);
						}
						break;
					case Attribute:
					case InternalAttribute:
						Attribute attribute = (Attribute) data;
						String value = attribute.getValue();
						blueRequest.putPost(name, value);
						logger.info("Form data, name: {}, value: {}", name, value);
						break;
					default:
						logger.warn("Unsupported HttpDataType: {}", data.getHttpDataType());
						break;
				}
			}
		} catch (HttpPostRequestDecoder.EndOfDataDecoderException e) {
			logger.debug("End of data decode");
		} catch (IOException e) {
			logger.warn("File upload raised exception, ", e);
		}
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		super.exceptionCaught(ctx, cause);
		Channel ch = ctx.channel();
		ChannelId id = ch.id();
		logger.error("Http client raised exception，disconnected: " + ch.remoteAddress() + "，id=" + id, cause);
		if (ctx.channel().isActive()) {
			HttpServerUtil.sendError(ch, HttpResponseStatus.INTERNAL_SERVER_ERROR);
		}
	}

}
